package cn.kennylee.codehub.mybatis.das.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONConfig;
import cn.hutool.json.JSONUtil;
import cn.kennylee.codehub.common.das.CondOperation;
import cn.kennylee.codehub.common.das.DasDbType;
import cn.kennylee.codehub.common.das.DasStrategyFactory;
import cn.kennylee.codehub.common.das.dto.PageDto;
import cn.kennylee.codehub.common.das.dto.SortDto;
import cn.kennylee.codehub.mybatis.das.eo.BaseEo;
import cn.kennylee.codehub.mybatis.das.extension.ComBaseDas;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.interfaces.Compare;
import com.baomidou.mybatisplus.core.conditions.interfaces.Func;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.query.MPJQueryWrapper;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.sql.DataSource;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static cn.kennylee.codehub.common.das.CondOperation.*;

/**
 * <p> Das工具类 </p>
 * <p>Created on 2024/4/21.</p>
 *
 * @author kennylee
 * @since 0.0.1
 */
@Slf4j
public class DasHelper {

    public static final String PK_FIELD_NAME = "id";

    /**
     * 默认是否转义like查询
     */
    @Setter
    @Getter
    public static boolean defaultEscapeLike = false;

    public static final int MAX_PAGE_SIZE = 1000;

    /**
     * 根据eo类的com.baomidou.mybatisplus.annotation.TableId注解，获取主键字段名
     *
     * @param entityClass 实体类
     * @return 主键字段名
     */
    @Nullable
    @SuppressWarnings("rawtypes")
    public static String getEntityPkDbName(Class<? extends BaseEo> entityClass) {
        TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
        if (false == tableInfo.havePK()) {
            return null;
        }
        return tableInfo.getKeyProperty();
    }

    /**
     * 根据eo类的com.baomidou.mybatisplus.annotation.TableId注解，通过反射获取主键字段值
     *
     * @param entity 实体对象
     * @return 主键值
     */
    @SneakyThrows
    @SuppressWarnings("unchecked")
    public static <P extends Serializable, T extends BaseEo<P>> P getEntityPkValue(T entity) {

        String pkFieldName = getEntityPkDbName(entity.getClass());

        if (Objects.isNull(pkFieldName)) {
            throw new IllegalArgumentException("获取实体类主键值失败");
        }

        return (P) ReflectUtil.getFieldValue(entity, pkFieldName);
    }

    /**
     * 根据eo类的com.baomidou.mybatisplus.annotation.TableId注解，通过反射设置主键字段值
     *
     * @param entity 实体对象
     * @param id     设置的主键值
     * @param <P>    主键类型
     * @param <T>    实体类型
     */
    @SneakyThrows
    public static <P extends Serializable, T extends BaseEo<P>> void setEntityPkValue(T entity, P id) {
        String pkFieldName = getEntityPkDbName(entity.getClass());

        if (Objects.isNull(pkFieldName)) {
            throw new IllegalArgumentException("获取实体类主键值失败");
        }
        ReflectUtil.setFieldValue(entity, pkFieldName, id);
    }

    public static <M extends Serializable, T extends BaseEo<M>> void parseWhereExcludeId(UpdateWrapper<T> updateWrapper, @NonNull T entity) {
        final String pkDbName = getEntityPkDbName(entity.getClass());
        Assert.notNull(pkDbName, "实体类没有主键注解");
        // key为属性名，下划线
        final Map<String, Object> notNullPropsMap = BeanUtil.beanToMap(entity, true, true);
        notNullPropsMap.forEach((key, value) -> {
            if (false == key.equalsIgnoreCase(pkDbName)) {
                updateWrapper.eq(key, value);
            }
        });
    }

    public static <M extends Serializable, T extends BaseEo<M>> void parseWhere(QueryWrapper<T> queryWrapper, @NonNull T entity) {
        // key为属性名，下划线
        final Map<String, Object> notNullPropsMap = BeanUtil.beanToMap(entity, true, true);
        notNullPropsMap.forEach(queryWrapper::eq);
    }

    /**
     * 构建查询第一页一条信息的分页参数，不count
     *
     * @return 分页参数
     */
    public static <T> IPage<T> buildSelectOnePage() {
        return Page.of(1, 1, false);
    }

    /**
     * 构建查分页参数，不count
     *
     * @return 分页参数
     */
    public static <T> IPage<T> buildPage(int size) {
        return Page.of(1, size, false);
    }

    /**
     * 构建查询单页最大的分页参数，不count
     *
     * @return 分页参数
     */
    public static <T> IPage<T> buildMaxPage() {
        return Page.of(1, MAX_PAGE_SIZE, false);
    }

    /**
     * <p>把当前的请求属性设置为可继承的</p>
     * <p>解决多线程下，获取当前用户名信息失效的问题</p>
     */
    public static void setCurrentAttrsInheritable() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (Objects.isNull(requestAttributes)) {
            log.warn("当前线程没有requestAttributes，设置子线程可继承失败");
        } else {
            RequestContextHolder.setRequestAttributes(requestAttributes, true);
        }
    }

    public static ServletRequestAttributes getCurrentRequestAttrs() {
        return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    }

    public static void setCurrentRequestAttrs(ServletRequestAttributes servletRequestAttributes) {
        RequestContextHolder.setRequestAttributes(servletRequestAttributes);
    }

    /**
     * <p>生成主键id</p>
     *
     * @return 主键id
     */
    public static long generateId() {
        return IdUtil.getSnowflakeNextId();
    }

    /**
     * <p>查询DTO实例转ExtQueryChainWrapper，动态添加eq和like条件</p>
     * <p>注：like条件默认不转义</p>
     *
     * @param searchExample 查询DTO，非null字段作为查询条件
     * @param eoClazz       目标查询EO
     * @param ignoreProps   忽略的属性列表
     * @param <T>           目标查询EO
     * @param <P>           EO的主键类型
     * @return QueryChainWrapper
     */
    @SuppressWarnings("unchecked")
    public static <T extends BaseEo<P>, P extends Serializable> MPJQueryWrapper<T> buildChainWrapperWithLike(final Object searchExample, Class<T> eoClazz, Collection<String> ignoreProps) {
        return (MPJQueryWrapper<T>) Objects.requireNonNull(DasStrategyFactory.getInstance().getStrategy(DasDbType.MYSQL)).parseQuery(searchExample, eoClazz, ignoreProps);
    }

    public static <T extends BaseEo<P>, P extends Serializable> MPJQueryWrapper<T> buildChainWrapperWithLike(final Object searchExample, Class<T> eoClazz) {
        return buildChainWrapperWithLike(searchExample, eoClazz, Collections.emptyList());
    }

    /**
     * <p>查询DTO实例转ExtQueryChainWrapper，便于后续扩展查询</p>
     * <p>将查询实例的键值转换为精确查询条件</p>
     *
     * @param queryDto     查询DTO，非null字段作为查询条件
     * @param abstractDas  das服务类
     * @param ignoreFields 忽略的字段，驼峰风格
     * @param <T>          目标查询EO
     * @param <P>          EO的主键类型
     * @return ExtQueryChainWrapper
     */
    public static <T extends BaseEo<P>, P extends Serializable> MPJQueryWrapper<T> buildChainWrapper(
        final Object queryDto, ComBaseDas<T, P> abstractDas, Class<T> eoClazz, @NonNull Set<String> ignoreFields) {

        if (log.isDebugEnabled()) {
            log.debug("查询的DTO实例: {}", JSONUtil.toJsonStr(queryDto, JSONConfig.create().setIgnoreNullValue(true)));
            log.debug("忽略的字段: {}", JSONUtil.toJsonStr(ignoreFields));
        }

        // 提取EO有的属性
        final T queryParam = convertObject(queryDto, eoClazz);

        // key为属性名，下划线
        final Map<String, Object> notNullPropsMap = BeanUtil.beanToMap(queryParam, true, true);

        if (log.isDebugEnabled()) {
            log.debug("查询的DTO实例转换后的map: {}", JSONUtil.toJsonStr(notNullPropsMap, JSONConfig.create().setIgnoreNullValue(true)));
        }

        MPJQueryWrapper<T> extQueryChainWrapper = abstractDas.filter(eoClazz);
        notNullPropsMap.forEach((field, val) -> {
            // field是下划线风格，ignore的驼峰的
            if (!ignoreFields.contains(StrUtil.toCamelCase(field))) {
                extQueryChainWrapper.eq(field, val);
            }
        });

        return extQueryChainWrapper;
    }

    /**
     * <p>添加模糊查询条件</p>
     *
     * @param extQueryChainWrapper 查询chainWrapper
     * @param queryDto             查询DTO，非null字段作为查询条件
     * @param likeFields           模糊查询的字段名，驼峰风格
     */
    public static void addLikeConditions(final Compare<?, String> extQueryChainWrapper,
                                         final Object queryDto,
                                         @NonNull Set<String> likeFields) {
        if (likeFields.isEmpty()) {
            return;
        }

        for (String likeField : likeFields) {
            Object val = BeanUtil.getProperty(queryDto, likeField);
            if (Objects.nonNull(val)) {
                extQueryChainWrapper.like(StrUtil.toUnderlineCase(likeField), val);
            }
        }
    }

    /**
     * <p>添加模糊查询条件</p>
     *
     * @param extQueryChainWrapper 查询chainWrapper
     * @param queryDto             查询DTO，非null字段作为查询条件
     * @param eoClazz              目标查询EO的类
     * @param <T>                  目标查询EO
     */
    @SuppressWarnings("rawtypes")
    public static <T extends BaseEo> void addLikeConditions(final Compare<?, String> extQueryChainWrapper,
                                                            final Object queryDto,
                                                            Class<T> eoClazz) {
        addLikeConditions(extQueryChainWrapper, queryDto, eoClazz, defaultEscapeLike);
    }

    /**
     * <p>根据searchDto定义属性的命名规则，添加模糊查询条件</p>
     *
     * @param extQueryChainWrapper 查询chainWrapper
     * @param queryDto             查询DTO，非null字段作为查询条件
     * @param eoClazz              目标查询EO的类
     * @param isEscape             是否转义
     * @param <T>                  目标查询EO
     */
    @SuppressWarnings({"rawtypes"})
    public static <T extends BaseEo> void addLikeConditions(final Compare<?, String> extQueryChainWrapper,
                                                            final Object queryDto,
                                                            Class<T> eoClazz,
                                                            boolean isEscape) {

        final Object escapeSearchDto = isEscape ? escapeLikeFields(queryDto) : queryDto;

        // key为属性名，下划线
        final Map<String, Object> notNullPropsMap = BeanUtil.beanToMap(escapeSearchDto, true, true);

        // like结尾的属性map，key为查询dto的原始名，value为去掉like后的eo属性名
        final Map<String, String> likeFieldMap = notNullPropsMap.keySet().stream()
            .filter(it -> LIKE.isEndWithIgnoreCase(it) &&
                // 非前或后模糊查询
                !(PRE_LIKE.isEndWithIgnoreCase(it) || SUF_LIKE.isEndWithIgnoreCase(it)))
            .collect(Collectors.toMap(Function.identity(), LIKE::subFieldNameUnderlineCase));

        if (log.isDebugEnabled()) {
            log.debug("like结尾的属性map: {}", JSONUtil.toJsonStr(likeFieldMap));
        }

        if (notNullPropsMap.isEmpty()) {
            return;
        }

        // 根据eo，获取能支持的属性集，转大写
        final Set<String> eoLikeProps = Arrays.stream(ReflectUtil.getFields(eoClazz))
            .map(field -> StrUtil.toUnderlineCase(field.getName()).toUpperCase())
            .collect(Collectors.toSet());

        likeFieldMap.forEach((queryPropName, eoPropName) -> {
            if (!eoLikeProps.contains(eoPropName.toUpperCase())) {
                log.debug("EO中不包含属性：{}", eoPropName);
                return;
            }
            // 获取查询dto的值
            Object val = BeanUtil.getProperty(escapeSearchDto, StrUtil.toCamelCase(queryPropName));

            if (Objects.nonNull(val)) {
                extQueryChainWrapper.like(eoPropName, val);
            }
        });
    }

    /**
     * <p>添加模糊查询条件</p>
     *
     * @param extQueryChainWrapper 查询chainWrapper
     * @param searchDto            前端搜索参数实例
     * @param likeFieldMap         模糊查询的字段名，驼峰风格。key为search dto的property，value为eo的property
     */
    public static void addLikeConditions(final Compare<?, String> extQueryChainWrapper,
                                         final Object searchDto,
                                         @NonNull Map<String, String> likeFieldMap) {
        if (likeFieldMap.isEmpty()) {
            return;
        }

        for (Map.Entry<String, String> entry : likeFieldMap.entrySet()) {
            // 获取查询dto的值
            Object val = BeanUtil.getProperty(searchDto, entry.getKey());

            if (Objects.nonNull(val)) {
                extQueryChainWrapper.like(StrUtil.toUnderlineCase(entry.getValue()), val);
            }
        }
    }

    /**
     * <p>添加基础的唯一的排序条件</p>
     *
     * @param extQueryChainWrapper 添加排序的目标查询实例
     */
    public static void addBaseUniqueSort(final Func<?, String> extQueryChainWrapper) {
        // 兜底一个唯一的排序
        extQueryChainWrapper.orderByDesc(PK_FIELD_NAME);
    }

    /**
     * <p>转义对象中使用在like查询中的值</p>
     *
     * @param obj        目标对象
     * @param fieldNames like的字段，驼峰风格命名
     * @param <T>        目标对象类型
     * @return 转义like查询值后的对象
     */
    public static <T> T escapeLikeFields(@NonNull T obj, @NonNull Collection<String> fieldNames) {
        // clone
        @SuppressWarnings("unchecked")
        T escapeObj = (T) convertObject(obj, obj.getClass());

        if (CollUtil.isEmpty(fieldNames)) {
            return escapeObj;
        }

        for (String fieldName : fieldNames) {
            String val = BeanUtil.getProperty(escapeObj, fieldName);

            if (StrUtil.isNotBlank(val)) {
                String escapeVal = StrUtil.replace(val, "%", "\\%");
                escapeVal = StrUtil.replace(escapeVal, "_", "\\_");

                BeanUtil.setFieldValue(escapeObj, fieldName, escapeVal);
            }
        }

        return escapeObj;
    }

    /**
     * <p>转义对象中使用在后缀为like的属性值，返回新的对象</p>
     *
     * @param obj 目标对象
     * @param <T> 目标对象类型
     * @return 转义like查询值后的对象
     */
    public static <T> T escapeLikeFields(@NonNull T obj) {

        final Map<String, Object> notNullPropsMap = BeanUtil.beanToMap(obj, false, true);

        final List<String> fieldNames = notNullPropsMap.keySet().parallelStream()
            .filter(LIKE::isEndWithIgnoreCase)
            .filter(it -> !(PRE_LIKE.isEndWithIgnoreCase(it) || SUF_LIKE.isEndWithIgnoreCase(it)))
            .collect(Collectors.toList());
        return escapeLikeFields(obj, fieldNames);
    }

    /**
     * 根据dto与eo，获取dto中满足like后缀且非空的属性名
     *
     * @param baseDto 查询dto
     * @param eoClazz 目标查询EO
     * @param <T>     目标查询EO
     * @return key为search dto的property，value为eo的property
     */
    @NonNull
    @SuppressWarnings({"rawtypes"})
    public static <T extends BaseEo> Map<String, String> getLikeFieldsBySuffix(final Object baseDto, Class<T> eoClazz) {
        // 根据dto，获取属性map，key为驼峰
        final Map<String, Object> notNullDtoPropsMap = BeanUtil.beanToMap(baseDto, false, true);

        if (CollUtil.isEmpty(notNullDtoPropsMap)) {
            return Collections.emptyMap();
        }

        // 根据eo，获取能支持的属性集，转大写,
        // key为属性名+like连接成的大写字符串，value为原始eo的property
        final Map<String, String> eoLikeProps = Arrays.stream(ReflectUtil.getFields(eoClazz))
            .map(Field::getName)
            .collect(Collectors.toMap(propName -> propName.concat(LIKE.lowerCase()).toUpperCase(), Function.identity(), (a, b) -> a));

        return notNullDtoPropsMap.keySet().stream()
            // eo支持的like
            .filter(prop -> eoLikeProps.containsKey(prop.toUpperCase()))
            .flatMap(prop -> {
                Map<String, String> likePropMap = MapUtil.newHashMap();
                likePropMap.put(prop, eoLikeProps.get(prop.toUpperCase()));
                return likePropMap.entrySet().stream();
            })
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    /**
     * <p>添加排序条件</p>
     *
     * @param extQueryChainWrapper 查询chainWrapper
     * @param pageDto              分页参数
     * @return 添加的排序数量
     */
    public static int addSorts(final Func<?, String> extQueryChainWrapper,
                               final PageDto pageDto) {
        if (CollUtil.isEmpty(pageDto.getSorts())) {
            return 0;
        }

        final List<SortDto> noneEmptyFields = pageDto.getSorts()
            .stream()
            .filter(it -> StrUtil.isAllNotBlank(it.getField(), it.getDirection()))
            .toList();

        int count = 0;
        for (SortDto sort : noneEmptyFields) {
            if (SortDto.ASC.equalsIgnoreCase(sort.getDirection())) {
                extQueryChainWrapper.orderByAsc(StrUtil.toUnderlineCase(sort.getField()));
                count++;
                continue;
            }
            if (SortDto.DESC.equalsIgnoreCase(sort.getDirection())) {
                extQueryChainWrapper.orderByDesc(StrUtil.toUnderlineCase(sort.getField()));
                count++;
            }
        }
        return count;
    }

    /**
     * 添加大于，大于等于，小于，小于等于的条件
     *
     * @param queryChainWrapper 查询chainWrapper
     * @param searchDto         查询参数
     * @param eoClazz           目标查询EO class
     * @param <T>               目标查询EO
     */
    @SuppressWarnings({"rawtypes"})
    public static <T extends BaseEo> void addRangeConditions(final Compare<?, String> queryChainWrapper,
                                                             final Object searchDto, Class<T> eoClazz) {
        // key为属性名，下划线
        final Map<String, Object> notNullPropsMap = BeanUtil.beanToMap(searchDto, true, true);

        // like结尾的属性map，key为查询dto的原始名，value为去掉like后的eo属性名
        final Map<String, String> likeFieldMap = notNullPropsMap.keySet().stream()
            .filter(CondOperation::isRangeField)
            .collect(Collectors.toMap(Function.identity(), it -> StrUtil.subBefore(it, "_", true)));

        if (log.isDebugEnabled()) {
            log.debug("范围参数的属性map: {}", JSONUtil.toJsonStr(likeFieldMap));
        }

        if (notNullPropsMap.isEmpty()) {
            return;
        }

        // 根据eo，获取能支持的属性集，转大写
        final Set<String> eoLikeProps = Arrays.stream(ReflectUtil.getFields(eoClazz))
            .map(field -> StrUtil.toUnderlineCase(field.getName()).toUpperCase())
            .collect(Collectors.toSet());

        likeFieldMap.forEach((queryPropName, eoPropName) -> {
            if (!eoLikeProps.contains(eoPropName.toUpperCase())) {
                log.debug("EO中不包含属性：{}", eoPropName);
                return;
            }
            // 获取查询dto的值
            Object val = BeanUtil.getProperty(searchDto, StrUtil.toCamelCase(queryPropName));

            if (Objects.nonNull(val)) {
                String eoFileName = StrUtil.toUnderlineCase(eoPropName);
                if (GT.isEndWithIgnoreCase(queryPropName)) {
                    queryChainWrapper.gt(eoFileName, val);
                }
                if (GE.isEndWithIgnoreCase(queryPropName)) {
                    queryChainWrapper.ge(eoFileName, val);
                }
                if (LT.isEndWithIgnoreCase(queryPropName)) {
                    queryChainWrapper.lt(eoFileName, val);
                }
                if (LE.isEndWithIgnoreCase(queryPropName)) {
                    queryChainWrapper.le(eoFileName, val);
                }
            }
        });
    }

    /**
     * 从源对象中，转换指定类型的对象，忽略null值
     *
     * @param source      源对象
     * @param targetClass 目标对象类型
     * @param <T>         目标对象类型
     * @return 目标对象
     */
    private static <T> T convertObject(@NonNull Object source, @NonNull Class<T> targetClass) {
        return BeanUtil.toBean(source, targetClass, CopyOptions.create().ignoreNullValue());
    }

    /**
     * 获取数据库类型
     *
     * @param dataSource 数据源
     * @return 数据库类型
     * @throws SQLException SQL异常
     */
    public static DbType getDbType(DataSource dataSource) throws SQLException {
        try (Connection connection = dataSource.getConnection()) {
            DatabaseMetaData metaData = connection.getMetaData();
            String databaseProductName = metaData.getDatabaseProductName();
            return DbType.getDbType(databaseProductName);
        }
    }

    /**
     * 判断是否是MySQL
     *
     * @param dataSource 数据源
     * @return 是否是MySQL
     * @throws SQLException SQL异常
     */
    public static boolean isMySql(DataSource dataSource) throws SQLException {
        DbType dbType = getDbType(dataSource);
        return Objects.equals(dbType, DbType.MYSQL);
    }

    /**
     * 判断是否是H2
     *
     * @param dataSource 数据源
     * @return 是否是H2
     * @throws SQLException SQL异常
     */
    public static boolean isH2(DataSource dataSource) throws SQLException {
        DbType dbType = getDbType(dataSource);
        return Objects.equals(dbType, DbType.H2);
    }

    /**
     * 是否可以使用MySQL语法
     *
     * @param dataSource 数据源
     * @return 是否可以使用MySQL语法
     */
    public static boolean isSupportsMySqlSyntax(@NonNull DataSource dataSource) {
        try {
            DbType dbType = getDbType(dataSource);
            return switch (dbType) {
                case MYSQL, MARIADB, H2, GAUSS, POSTGRE_SQL -> true;
                default -> false;
            };
        } catch (SQLException e) {
            log.error("获取数据库类型失败", e);
            return false;
        }
    }

    /**
     * 是否支持Oracle语法
     *
     * @param dataSource 数据源
     * @return 是否支持Oracle语法
     */
    public static boolean isSupportsOracleSyntax(@NonNull DataSource dataSource) {
        try {
            DbType dbType = getDbType(dataSource);
            return switch (dbType) {
                case ORACLE, ORACLE_12C, DM -> true;
                default -> false;
            };
        } catch (SQLException e) {
            log.error("获取数据库类型失败", e);
            return false;
        }
    }
}
